{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Object-oriented programming" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Introduction" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Object-oriented programming is a way of programming that groups together related data and functions into **objects**, and makes it easier to design more complex programs. In traditional (procedural) programming, you can typically think of a program as an set of instructions that is read from top to bottom, with the exception of functions, which allow you to avoid repetitive code. In object-oriented programming, one instead thinks of a program mainly as setting up a number of 'types' of objects, and doing operations on them.\n", "\n", "Note that you have already been using objects! Every Python object is an object:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s = 'hello'\n", "s.upper()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "l = [4,2,1]\n", "l.sort()\n", "l" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In these cases, ``upper`` and ``sort`` are both **methods**, and ``s`` and ``l`` are **instances** of the ``str`` and ``list`` **class** respectively (we will discuss these terms below)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Object oriented programming unifies attributes and behavior within a single definition, called a **class**. When you define a class (e.g. a 'NuclearPowerPlant'), you can provide it with some **attributes** (this can be data that describes it. e.g. name, geolocation, status, maximum_output etc.) and some **behaviors (methods)** available to it (which you will defined as functions inside the class e.g. 'start_up', 'shut_down', 'lower_control_rods' etc)\n", "\n", "Classes are generic definitions. **Instances** of a class (called **objects**), are specific realisations of that class. For example, you could define a 'FukushimaPowerPlant' as a specific instance of the 'NuclearPowerPlant' class." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Classes, instances, and methods" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Classes are most easily explained by example, so let's dive right in and look at a **class**, which is used to define an **object**:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Person(object):\n", " \n", " def __init__(self, name):\n", " self.name = name\n", " \n", " def say_hello(self):\n", " print(\"Hello, my name is \" + self.name)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "and let's try and use it:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tom = Person('Tom')\n", "tom.say_hello()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There is a lot of new syntax here which you won't have likely seen before, but also some syntax which will look more familiar. First, let's have a look at the class definition. The class is defined using the following syntax:\n", "\n", " class Person(object):\n", " ...\n", "\n", "This looks similar to the definition for a function, except that it doesn't directly contain code, but it then contains a series of functions:\n", "\n", " class Person(object):\n", " \n", " def __init__(self, ...):\n", " ...\n", "\n", " def say_hello(self, ...):\n", " ...\n", "\n", "So in effect, a class is a collection of functions. What is special about these functions? If you look at the full class definition above, you will see that the first argument to all the functions is ``self``, which is the object itself. Why is this useful?\n", "\n", "At this point, we need to clarify some more terminology: an **instance** of a class is a particular object represented by the **class** - that is, while ``Person`` is the class (i.e. the general definition of the idea of a 'person'), an instance is a *particular* person, e.g. Tom:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tom = Person('Tom')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here ``tom`` is an instance of the ``Person`` class. Now let's look at the first defined **method**, called ``__init__``:\n", "\n", " def __init__(self, name):\n", " self.name = name\n", "\n", "The term **method** is basically used for a function that is attached to a class. The ``__init__`` method is a method that is automatically called when creating an instance of the class (for those familiar with the terminology, it is equivalent to a constructor). What is basically happening here is that ``self`` is the actual instance that is being created, and the method then takes the name of the person and assigns it to the ``name`` **attribute** of the instance:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tom = Person('Tom')\n", "print(tom.name)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, the ``tom`` instance has an attribute ``name`` that has been set to the name that was passed. Now let's look at the second method:\n", "\n", " def say_hello(self):\n", " print \"Hello, my name is \" + self.name\n", "\n", "This looks more like a normal function, but takes the same ``self`` argument. It then prints out a string containing the value of the ``name`` attribute *of this instance*. Note that while all methods should take the ``self`` argument, this argument doesn't need to be passed because when calling a method, this is automatically done:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tom.say_hello()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "is equivalent to:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Person.say_hello(tom)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the last example, we passed the instance explicitly to the function, but the first notation is much cleaner and simpler. Now let's look at the following example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "alice = Person(\"Alice\")\n", "bob = Person(\"Bob\")\n", "alice.say_hello()\n", "bob.say_hello()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "as you can see, when calling ``say_hello``, the result will depend to the actual object that the method is attached to.\n", "\n", "Since they are essentially functions, methods can of course take arguments, which can be any Python object(s). For example, we can make a new ``say_hello_to`` method that will take another ``Person`` instance and say hello to them:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Person(object):\n", " \n", " def __init__(self, name):\n", " self.name = name\n", " \n", " def __str__(self):\n", " return \"The Person {}\".format(self.name)\n", " \n", " def __repr__(self):\n", " return \"{}({})\".format(self.__class__.__name__,\n", " repr(self.name))\n", " \n", " def say_hello(self):\n", " print(\"Hello, my name is \" + self.name)\n", " \n", " def say_hello_to(self, other):\n", " print(\"Hello \" + other.name + \", my name is \" + self.name)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "alice = Person(\"Alice\")\n", "bob = Person(\"Bob\")\n", "alice.say_hello_to(bob)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we will see below, you can actually pass any object to ``say_hello_to`` as long as it has a ``name`` attribute." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note:\n", "* the 'str' definition serves to allow str to act on an instance of the Person class.\n", "* the 'repr' definition serves to allows python to say what kind of object this is. If str does not exist, python will use repr." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bob # using the repr functionality" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Inheritance" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One of the powerful features of object-oriented programming is **inheritance**, which means that it is possible to define classes based on other classes. For example, we can define:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from scipy.integrate import simps\n", "\n", "class Scientist(Person):\n", " \n", " def integrate(self, x, y):\n", " return simps(y, x=x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This looks similar to before, but this time when defining the class, we've used ``Person`` instead of ``object``. This means that by default, ``Scientist`` will behave like a ``Person`` instance, but it then has some additional methods defined:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "alice = Scientist(\"Alice\")\n", "alice.integrate([1,2,3], [4,5,6])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The ``say_hello_to`` method takes any object that has a ``name`` attribute, so it can say hello to another person:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "alice" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bob = Person(\"Bob\")\n", "alice.say_hello_to(bob)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "or scientist:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "eve = Scientist(\"Eve\")\n", "alice.say_hello_to(eve)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can check if an instance belongs to a particular class like this:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "if isinstance(alice, Scientist):\n", " print ('Alice is a Scientist and can integrate!')\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Attributes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As mentioned above, attributes are variables attached to the object. It is worth mentioning that attributes are *not* static, so they can be changed from outside the class:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p = Person('Tom')\n", "p.say_hello()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.name = 'Alice'\n", "p.say_hello()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "and it is also possible to create new attributes from the outside:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.age = 97\n", "print(p.name)\n", "print(p.age)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This last practice (creating attributes in an object) is typically a bad idea, because you're essentially violating the object's “namespace”, the names the object manages, and it's not hard to break things in this way.\n", "\n", "Then again, python does not keep you from doing such things because that would keep you from doing smart things." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A good way to think about objects is to say:\n", "\n", "* attributes = state\n", "* methods = behaviour" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Why use objects?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So far, the programs you have needed to write have not been very complex, but as you write more and more complex analysis, classes may come in useful in some situations. For example, if you are doing particle physics, you might want to use a class to represent a ``Particle``, which then has common operations and calculations you might want to do with a particle. If you are doing Astronomy, you could use a ``Star`` or ``Galaxy`` class that would be used to represent these objects. You can even use objects without defining actual methods, but just as a convenience to contain several variables - if you need to always handle cartesian point in your code, you could define:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Point(object):\n", " def __init__(self, x, y, z):\n", " self.x = x\n", " self.y = y\n", " self.z = z" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pt = Point(1, 2, 3)\n", "pt.x" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After which if you want to pass a point to a function, you can pass it in a single variable instead of three:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def find_separation(p1, p2):\n", " return np.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2 + (p1.z - p2.z)**2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "as opposed to:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def find_separation(x1, y1, z1, x2, y2, z2):\n", " return np.sqrt((x1 - x2)**2 + (y1 - y2)**2 + (z1 - z2)**2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This might not look like a big difference, but now imagine that you also wanted to pass 3-d velocities, then you would need to call the function with 12 arguments!\n", "\n", "This is not to say that you should *always* use objects, but if you start to think of your program instead of what objects and basic operations are being represented, you may be able to write it more simply than if you were using only functions and procedural code. You may also be able to re-use classes for different projects if they are general enough!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. Write a ``Particle`` class that can be used to represent a particle with a mass, a 3-d position, and a 3-d velocity.\n", "\n", "2. Write a method that can be used to compute the kinetic energy of the particle\n", "\n", "3. Write a method that takes another particle as an argument and finds the distance between the two particles\n", "\n", "4. Write a method that given a time interval ``dt`` will update the position of the particle to the new position based on the current position and velocity.\n", "\n", "5. Write a ``ChargedParticle`` class that inherits from the ``Particle`` class, but also has an attribute for the charge of the particle.\n", "\n", "6. Write examples of using these classes to test that the methods are working correctly." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# your solution here\n" ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" } }, "nbformat": 4, "nbformat_minor": 1 }